package com.imooc.uaa.rest;

import com.imooc.uaa.domain.Auth;
import com.imooc.uaa.domain.MfaType;
import com.imooc.uaa.domain.User;
import com.imooc.uaa.domain.dto.LoginDto;
import com.imooc.uaa.domain.dto.SendTotpDto;
import com.imooc.uaa.domain.dto.UserDto;
import com.imooc.uaa.domain.dto.VerifyTotpDto;
import com.imooc.uaa.exception.*;
import com.imooc.uaa.service.EmailService;
import com.imooc.uaa.service.SmsService;
import com.imooc.uaa.service.UserCacheService;
import com.imooc.uaa.service.UserService;
import com.imooc.uaa.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.springframework.data.util.Pair;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.nio.file.AccessDeniedException;

@RequiredArgsConstructor
@RestController
@RequestMapping("/authorize")
public class AuthorizeResource {

    private final UserService userService;
    private final JwtUtil jwtUtil;
    private final UserCacheService userCacheService;
    private final SmsService smsService;
    private final EmailService emailService;

    @PostMapping("/register")
    public void register(@RequestBody @Valid UserDto userDto) {
        // 1. 检查 username, email, mobile 都是唯一的，所以有查询数据库确保唯一
        if (userService.isUsernameExist(userDto.getUsername())) {
            throw new DuplicateProblem("用户名重复");
        }
        if (userService.isEmailExist(userDto.getEmail())) {
            throw new DuplicateProblem("电子邮件重复");
        }
        if (userService.isMobileExist(userDto.getMobile())) {
            throw new DuplicateProblem("手机号重复");
        }
        // 2. 我们需要  userDto 转换成 User,
        val user = User.builder()
                .username(userDto.getUsername())
                .email(userDto.getEmail())
                .mobile(userDto.getMobile())
                .password(userDto.getPassword())
                .build();
        // 3. 我们给一个默认角色（ROLE_USER）, 然后保存。
        userService.register(user);
    }

    @PostMapping("/token")
    public ResponseEntity<?> login(@Valid @RequestBody LoginDto loginDto) throws Exception {
        return userService.findOptionalByUsernameAndPassword(loginDto.getUsername(), loginDto.getPassword())
                .map(user -> {
                    // 1. 升级密码编码
                    userService.updatePassword(user, loginDto.getPassword());
                    // 2. 验证
                    if (!user.isEnabled()) {
                        throw new UserNotEnableProblem();
                    }
                    if (!user.isAccountNonExpired()) {
                        throw new UserAccountExpiredProblem();
                    }
                    if (!user.isAccountNonLocked()) {
                        throw new UserAccountLockedProblem();
                    }
                    if (!user.isCredentialsNonExpired()) {
                        throw new UserCredentialsNotExpired();
                    }
                    // 3. 判断 usingMfa 如果是 false, 我们就直接还回 token
                    if (!user.isUsingMfa()) {
                        return ResponseEntity.ok().body(userService.login(loginDto.getUsername(), loginDto.getPassword()));
                    }
                    // 4. 使用多因子认证
                    val mfaId = userCacheService.cacheUser(user);
                    // 5. "X-Authenticate": "mfa", "realm=" + mfaId
                    return ResponseEntity.
                            status(HttpStatus.UNAUTHORIZED)
                            .header("X-Authenticate", "mfa", "realm=" + mfaId)
                            .build();
                }).orElseThrow(() -> new BadCredentialsException("用户名或密码错误"));
    }

    @PutMapping("/totp")
    public void sendTotp(@Valid @RequestBody SendTotpDto sendTotpDto) {
        userCacheService.retrieveUser(sendTotpDto.getMfaId())
                .flatMap(user -> userService.createTotp(user.getMfaKey()).map(totp -> Pair.of(user, totp)))
                .ifPresentOrElse(pair -> {
                    if (sendTotpDto.getMfaType() == MfaType.SMS) {
                        smsService.send(pair.getFirst().getMobile(), pair.getSecond());
                    } else {
                        emailService.send(pair.getFirst().getMobile(), pair.getSecond());
                    }
                }, () -> {
                    throw new InvalidTotpProblem();
                });
    }

    @PostMapping("/totp")
    public Auth verifyTotp(@Valid @RequestBody VerifyTotpDto verifyTotpDto) {
         return userCacheService.verifyTotp(verifyTotpDto.getMfaId(), verifyTotpDto.getCode())
                 .map(user -> userService.login(user.getUsername(), user.getPassword()))
                 .orElseThrow(InvalidTotpProblem::new);

    }

    @PostMapping("/token/refresh")
    public Auth refreshToken(
            @RequestHeader(name = "Authorization") String authorization,
            @RequestParam String refreshToken) throws AccessDeniedException {
        val PREFIX = "Bearer";
        val accessToken = authorization.replace(PREFIX, "");
        if (jwtUtil.validateRefreshToken(refreshToken) && jwtUtil.validateAccessTokenWithoutExpiration(accessToken)) {
            return new Auth(jwtUtil.createAccessTokenWithRefreshToken(refreshToken), refreshToken);
        }
        throw new AccessDeniedException("访问被拒接");
    }
}
